短小

函数的第一规则是短小,第二规则是还要更短小。

if语句、else语句、while语句等其中的代码块应该只有一行,该行大抵是一个函数调用语句。

只做一件事

要判断函数是否不止做了一件事,就看是否还能再拆出一个函数。

switch语句

写出短小的switch语句很难,我们可以利用多态来实现switch语句的重构。

1
2
3
4
5
6
7
8
9
10
11
12
public Money calculatePay(Employee e) throws InvalidEmployeeType {
switch(e.type) {
case COMMISSIONED:
return calculateComissionPay(e);
case HOURLY:
return calculateHourlyPay(e);
case SALARIED:
return calculateSalariedPay(e);
default:
throw new InvalidEmployeeType(e.type);
}
}

该函数有好几个问题。首先,它太长,当出现新的雇员类型时,还会变得更长;其次,它明显做了不止一件事;第三,它违反了单一权责原则;第四,它违反了开放闭合原则,因为每当添加新类型时,就必须修改它。不过,该函数最麻烦的可能是到处皆有类似结构的函数,例如可能会有isPayDay(Employee e, Date d)deliverPay(Employee e, Money pay)

该问题的解决方案是将switch语句埋到抽象工厂底下,不让任何人看到。该工厂使用switch语句为Employee的派生物创建适当的实体,而不同的函数,如calculatePay、isPayday和deliverPay等,则借由Employee接口多态的接收派遣。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public abstract class Employee {
public abstract boolean isPayday();
public abstract Money calculatePay();
public abstract void deliverPay(Money money);
}
public interface EmployeeFactory {
public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType;
}
public class EmployeeFacvtoryImpl implements EmployeeFactory {
public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeTypen {
swicth (r.type) {
case COMMISSIONED:
return new CommissionedEmployee(r);
case HOURLY:
return new HourlyEmployee(r);
case SALARIED:
return new SalariedEmployee(r);
default:
throw new InvalidEmployeeType(r.type);
}
}
}

函数参数

最理想的参数数量是零,其次是一,再次是二,应尽量避免三。

标识参数丑陋不堪,向函数传入布尔值简直就是骇人听闻的做法,这样做,方法签名立刻变得复杂起来,大声宣布本函数不止做一件事,如果标识为true将会这样做,标识为false将会那样做。

对于二元函数来说,其不算恶劣,不过你应当尽量利用一些机制将其转换为一元函数。例如你可以把writeField方法写成outputStream的成员之一,从而可以这样用:outputStream.writeField(name)。或者你可以把outputStream写成当前类的成员变量,从而无需再传递它。

参数对象

如果函数看来需要两个、三个或三个以上参数,就说明其中一些参数应该封装成类了,例如下面两个声明的差别:

1
2
Circle makeCircle(double x, double y, double radius);
Circle makeCircle(Point center, double radius);

从参数传递对象,从而减少参数变量。

动词与关键字

给函数取个好名字,能较好的解释函数的意图以及参数的顺序和意图。对于一元函数,函数和参数应当形成一种非常良好的动词/名词对形式。例如,write(name)就相当令人认同,不管这个name是什么,都要被write。更好的名称大概是writeField(name),它告诉我们name是一个field。

我们可以将参数的名称编码进函数名,例如assertEqual改成assertExpectedEqualsActual(expected, actual)可能更好些,这大大减轻了记忆参数顺序的负担。

无副作用

函数承诺只做一件事,但是还是会做其他被隐藏起来的事,有时它会对自己类中的变量做出未能预期的改动。

使用异常代替返回错误码

从指令式函数返回错误码轻微违反了指令和询问分隔的规则,它鼓励了在if语句判断中把指令当做表达式使用:

1
if (deletePage(page) == E_OK)

当返回错误码时,就是在要求调用者立刻处理错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if (deletePage(page) == E_OK) {
if (registry.deleteReference(page.name) == E_OK) {
if (configKeys.deleteKey(page.name.makeKey()) == E_OK) {
logger.log("page deleted");
} else {
logger.log("configKey not deleted");
}
} else {
logger.log("deleteReference from registry failed");
}
} else {
logger.log("delete failed");
return E_ERROR;
}

另一方面,如果使用异常代替返回错误码,错误处理代码就能从主路径中分离出来,得到简化:

1
2
3
4
5
6
7
try {
deletePage(page);
registry.deleteReference(page.name);
configKeys.deleteKey(page.name.makeKey());
} catch (Exception e) {
logger.log(e.getMessage());
}

抽离try/catch代码块

try/catch代码块丑陋不堪,它们搞乱了代码结构,把错误处理与正常流程混为一谈,最好把try和catch代码块的主体部分抽离出来,另外形成函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void delete(Page page) {
try {
deletePageAndAllReferences(page);
} catch (Exception e) {
logError(e);
}
}
private void deletePageAndAllReferences(Page page) {
deletePage(page);
registry.deleteReference(page.name);
configKeys.deleteKey(page.name.makeKey());
}
private void logError(Exception e) {
logger.log(e.getMessage());
}

在上述例子中,delete函数只与错误处理有关,很容器理解然后忽略掉,deletePageAndAllReferences函数只与完全删除一个page有关,错误处理可以忽略掉,有了这样美妙的区隔,代码就更易于理解和修改了。

错误处理就是一件事

函数应该只做一件事,而错误处理就是一件事。因此,处理错误的函数不该做其他事,这意味着如果关键字try在某个函数中存在,它就该是这个函数的第一个单词,而且在catch/finally代码块后面也不该有其他内容。

依赖磁铁

返回错误码通常暗示某处有个类或者枚举,定义了所有错误码:

1
2
3
4
5
6
7
8
public enum Error {
OK,
INVALID,
NO_SUCH,
LOCKED,
OUT_OF_RESOURCES,
WAITING_FOR_EVENT;
}

这样的类就是一块依赖磁铁,其他许多类都得导入和使用它,当Error枚举修改时,所有这些其他的类都需要重新编译和部署,这对Error类造成了负面压力。程序员不愿意条件新的错误代码,因为这样他们就得重新构建和部署所有东西,于是他们就复用旧的错误码,而不添加新的。

使用异常替代错误码,新异常就可以从异常类派生出来,无需重新编译或重新部署